<h3 id="c-ms-widget">UI绑定(ms-widget)</h3>
<p>avalon能通过ms-widget属性将一个普通标签变成一个结构非常复杂功能非常丰富的组件，其语法为</p>
<pre class="brush:html;gutter:false;toolbar:false;">
      ms-widget="uiName, id?, optsName? "
</pre>
<ul>
    <li>uiName，必选，一定要全部字母小写，表示组件的类型</li>
    <li>id 可选 这表示新生成的VM的$id，方便我们从avalon.vmodels[id]中获取它操作它，如果它等于$，那么表示它是随机生成，与不写这个效果一样，框架会在uiName加上时间截，生成随机ID</li>
    <li>optName 可选， 配置对象的名字。这是指框架会找离它最近那个VM作为目标，然后在上面找与它同名的一个对象。如果你没有指定, 那么这个配置对象的名字与组件的名字同名。</li>
</ul>
<pre class="brush:html;gutter:false;toolbar:false;">
&lt;div ms-controller="xxx"&gt;
     &lt;div ms-controller="yyy"&gt;
        &lt;div ms-widget="dialog,$,$opt"&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;script&gt;
   avalon.define("xxx", function(vm){
        vm.uuu = "ssdf"     
   })
   avalon.define("yyy", function(vm){
        vm.dfd = "sdfdf"
        vm.$opt = {//这个对象是作为dialog的配置对象而存在
            width: 400,
            height: 200,
            toggle: false
        }
   })
&lt;/script&gt;
</pre>
<p>编写一个组件，我们非常注重它的可配置性。avalon的UI绑定拥有三处用于定义配置的地方。第一处，就是上面提到的，在一个已存的VM中定义一个对象（最好将它定义不可监听的，以$开头或放在$skipArray数组中）。
    它相当于一个父类。让一组UI共享相同的配置。第二处是位于UI绑定的构造器中，我们可以通过avalon.ui[widgetName].defaults访问到。它是让同一种组件的所有实例都共享相同的配置。第三处是在定义ms-widget所在的元素上，添加一些HTML5 data-*属性，格式为data- widgetName - optionName，比如你想为suggest组件定义一个叫toggle的配置项，那么就应该写作data-suggest-toggle，如果是一个叫currentValue,那么要将它改成"连字符风格"，即将大写变小写前面再加一横杠，data-suggest-current-value。它们是用来制定当前UI实例的。</p>
<pre class="brush:html;gutter:false;toolbar:false;">
 &lt;input ms-controller="bbb" ms-widget="datepicker"   data-datepicker-date-format="yyyy-MM-dd"&gt;
</pre>
<p>好了，我们正式介绍如何编写组件本身。我们要记住一点，<a href="https://github.com/RubyLouvre/avalon">avalon</a>所有操作都与扫描机制息息相关，就像jQuery喜欢把它的API选择器引擎绑架在一起。为什么这样说呢，因为视图与代码分定义在不同的地方，只有经过扫描后，视图中的绑定才会挟持它们所在的元素节点与VM关联在一起。框架会默认在domReady之后扫描一次。
    如果这时我们用到的组件所对应的JS文件还没有加载好，那么当加载好后我们需要自己手动扫描。</p>
<pre class="brush:javascript;gutter:false;toolbar:false;">
require(["avalon.dialog"], function() {
    avalon.define("test", function(vm) {
        vm.$skipArray = ["dialog"]
        vm.dialog = {
            buttons: [{text: "ok"}, {text: "cancel"}]
        }
    })
    avalon.scan()
})
</pre>
<p>组件大抵都是以下样子，留意一下里面的注释：</p>
<pre class="brush:javascript;gutter:false;toolbar:false;">
//avalon 1.2.5 2014.4.2
define(["avalon",
    "text!avalon.tabs.tab.html", //这是组件用到的VM
    "text!avalon.tabs.panel.html",
    "text!avalon.tabs.close.html"],
        function(avalon, tabHTML, panelHTML, closeHTML) {

            var widget = avalon.ui.tabs = function(element, data, vmodels) {
                var options = data.tabsOptions//★★★取得配置项

                var vmodel = avalon.define(data.tabsId, function(vm) {
                    avalon.mix(vm, options)//这视情况使用浅拷贝或深拷贝avalon.mix(true, vm, options)
                    vm.$init = function() {//初始化组件的界面，最好定义此方法，让框架对它进行自动化配置
                        avalon(element).addClass("ui-tabs ui-widget ui-widget-content ui-corner-all")
                        // ★★★设置动态模板，注意模块上所有占位符都以“MS_OPTION_XXX”形式实现
                        var tablist = tabHTML
                                .replace("MS_OPTION_EVENT", vmodel.event)
                                .replace("MS_OPTION_REMOVABLE", vmodel.removable ? closeHTML : "")
                        //决定是重复利用已有的元素，还是通过ms-include-src引入新内部
                        var contentType = options.contentType === "content" ? 0 : 1
                        var panels = panelHTML.split("MS_OPTION_CONTENT")[contentType]
                        element.innerHTML = vmodel.bottom ? panels + tablist : tablist + panels
                        element.setAttribute("ms-class-1", "ui-tabs-collapsible:collapsible")
                        element.setAttribute("ms-class-2", "tabs-bottom:bottom")
                        avalon.scan(element, [vmodel].concat(vmodels))
                        if(typeof vmodel.onInit === "function"){
                           vmodel.onInit.call(element, vmodel, options, vmodels)
                        }

                    }
                    vm.$remove = function() {//清空构成UI的所有节点，最好定义此方法，让框架对它进行自动化销毁
                        element.innerHTML =  ""
                    }
                    //其他属性与方法
                    vm.tabs = []
                    vm.tabpanels = []
                    vm.disable = function(index, disable) {
                       //具体实现
                    }
                    vm.enable = function(index) {
                      //具体实现
                    }
                    vm.add = function(config) {
                      //具体实现
                    }
                     vm.remove = function(config) {
                      //具体实现
                    }
                    vm.activate = function(event, index) {
                       //具体实现
                   }
                })
                return vmodel//必须返回组件VM
            }
            widget.defaults = {//默认配置项
                collapsed: false,
                active: 0, //默认打开第几个面板
                event: "click", //切换面板的事件，移过(mouseenter)还是点击(click)
                collapsible: false, //当切换面板的事件为click时，
                bottom: false, //按钮位于上方还是上方
                removable: false, //按钮的左上角是否出现X，用于移除按钮与对应面板
                activate: avalon.noop, // 切换面板后触发的回调
                contentType: "content"
            }
            return avalon
        })
</pre>
<p>这里需要着重留意的是data里面有两个属性,一个叫"组件名+Options"，是一个对象，如果它里面有widget+"Id"这个属性，那么新生成的VM就是用它作为它的$id。
    一个叫"组件名+Id"，就是新生成的VM的$id。组件必须注册到avalon.ui上，它的构造器必须定义一个叫defaults的默认配置项。</p>
<p>另外，由于扫描从外到里，当它扫描了ms-widget所在的元素，如果此元素里面还有子元素，
    并且它们的绑定属性需要用到新VM的某一些字段，这时让它继续扫下去就有出错的危险。我们可以先它的所有子元素放到一个文档碎片中。待到新VM中出来后，再插回原地置，然后手动扫描。</p>
<p>在avalon1.2.4中，ms-widget所在的元素还添加了一个msData对象，保存着所有ms-*属性，此外data属性还添加了ms-widget-id属性，保存着新生成的组件VM的ID。我们可以靠它们做更多的事。</p>
<p>在avalon1.2.5中，要求组件作者最好为VM添加$init, $remove, onInit方法，方便自动化初始化组件与对它进行销毁。销毁的条件是定义ms-widget的那个元素被移出DOM。有时我们不得不对此元素进行移动（移动时也会发生移出DOM树的操作），这时就需要注意啦，为元素添加一个msRetain，值为true，为可以避销自动化销毁。当它再插回DOM树，记得将此属性去掉。</p>
<p>下面是ms-widget的部分源码</p>
<pre class="brush:js;gutter:false;">
        data[widget + "Id"] = args[1]
        data[widget + "Options"] = avalon.mix({}, constructor.defaults, vmOptions, widgetData)
        element.removeAttribute("ms-widget")
        var vmodel = constructor(element, data, vmodels)//防止组件不返回VM
        data.evaluator = noop
        element.msData["ms-widget-id"] = vmodel.$id
        if (vmodel.hasOwnProperty("$init")) {//初始化组件
            vmodel.$init()
        }
        if (vmodel.hasOwnProperty("$remove")) {
            var offTree = function() {//销毁组件, CG回收
                vmodel.$remove()
                element.msData = {}
                delete VMODELS[vmodel.$id]
            }
            if (supportMutationEvents) {
                element.addEventListener("DOMNodeRemoved", function(e) {
                    if (e.target === this && !this.msRetain) {
                        offTree()
                    }
                })
            } else {
                element.offTree = offTree
                launchImpl(element)//如果不支持DOMNodeRemoved事件，使用一个全局的定时器进行轮询
            }
        }
</pre>
<fieldset>
    <legend>avalon OniUI的编写规范</legend>
    <ul>
        <li>
            组件基于avalon1.2.5构建，每个必须有$init, $remove，onInit方法， 以便让框架自行管理创建与销毁。$remove注意对存在子节点的元素进行内部清空，elem.innerHTML = elem.textContent = ""，注意移除组件内部绑定的事件回调。
            <blockquote>
                需要值得注意的是,$remove操作会在元素移出DOM树时触发,但普通的移动节点操作也会让元素离开DOM树,这时就会引发$remove操作,
                导致元素上面的组件被销毁.为防止误杀,我们在移动节点时,可以在元素定义msRetain属性,elem.msRetain = true,待到移动后再置为false
            </blockquote>
        </li>
        <li>
            组件的$init方法里面, 在扫描后最好再调用一个onInit回调,传入当前组件的 vmodel, options, vmodels, this指向当前元素 
            <blockquote>
                $init: function(){
                element.innerHTML = "ui-template"
                avalon.scan(element, _vmodels)
                if(typeof options.onInit === "function" ){
                //vmodels是不包括vmodel的
                options.onInit.call(element, vmodel, options, vmodels)
                }
                }
            </blockquote>
            <p>这样用户就不需要定义组件的$id了</p>
        </li>
        <li>
            组件在引入avalon的依赖时,直接define(["avalon"], fn)就行了,不需define(["../avalon"],fn)或define(["./ddd/avalon"],fn),因为avalon是一个核心模块，框架内部对它做了处理
        </li>
        <li>
            组件要最大程度兼容之前的，不行就两个，一个是兼容的，一个无牵制的
        </li>
        <li>
            组件的显示隐藏必须通过toggle属性, 对应onOpen,onClose回调
        </li>
        <li>
            组件是插在哪一个元素里面是由container属性决定,可以是ID或元素节点(这主要是那些弹出组件)
        </li>
        <li>把数组形式的数据源对应的配置项命名为data</li>
        <li>
            组件必须有widgetElement属性，引用元素本身
        </li>
        <li>
            有模板的组件的默认匹配项里必须getTemplate方法 ,用于修改template, template在vm中不可监控的
        </li>
        <li>尽量减举行监控元素，善用$skipArray。</li>
        <li>为元素绑定事件时，注意不要覆盖用户预置的ms-click, ms-keyup等事件</li>
        <li>
            组件必须有改写模板的配置项,参看@组件的popup
        </li>
        <li>
            组件的模板必须独立出来， 组件的模板与样式合而为一,通过MS_OPTION_STYLE分隔,在组件第一次加载时,添加到avalonStyle.cssText里面去
        </li>
        <li>
            组件的模板请使用MS_OPTION_*做占位符
        </li>
        <li>
            组件的回调名应该以onXxx开头
        </li>
        <li>
            组件的模板编写,请遵循<a href="http://www.cnblogs.com/rubylouvre/p/3642024.html">《avalon 的HTML规范》</a>
        </li>
    </ul>

</fieldset>
<p>大家可以参考下面官方组件编写自己的UI组件。</p>
<ul>
    <li><a href="https://github.com/RubyLouvre/avalon.oniui/blob/master/at/avalon.at.js">微博AT提示列表组件</a></li>
    <li><a href="https://github.com/RubyLouvre/avalon.oniui/blob/master/pager/avalon.pager.js">分页栏组件</a></li>
</ul>